﻿// inspired on: http://jfromaniello.blogspot.com/2009/12/linq-specifications-from-argentina-with.html
// Specification class by José F. Romaniello.
// and enhanced inspired on: Mono.Linq.Expressions

using System;
using System.ComponentModel;
using System.Linq.Expressions;

namespace AstoriaPlus
{
	[EditorBrowsable(EditorBrowsableState.Never)]
	public class QueryExpression<TEntity, TContext>
	{
		private readonly Expression<Func<TEntity, TContext, bool>> expression;

		public QueryExpression(Expression<Func<TEntity, TContext, bool>> expression)
		{
			this.expression = expression;
		}

		public static QueryExpression<TEntity, TContext> operator &(QueryExpression<TEntity, TContext> spec1, QueryExpression<TEntity, TContext> spec2)
		{
			return Combine(spec1, spec2, Expression.AndAlso);
		}

		public static QueryExpression<TEntity, TContext> operator |(QueryExpression<TEntity, TContext> spec1, QueryExpression<TEntity, TContext> spec2)
		{
			return Combine(spec1, spec2, Expression.OrElse);
		}

		public static QueryExpression<TEntity, TContext> operator !(QueryExpression<TEntity, TContext> spec)
		{
			var expression = spec.expression;
			var param1 = CreateParameterFrom(expression, 0);
			var param2 = CreateParameterFrom(expression, 1);

			return Expression.Lambda<Func<TEntity, TContext, bool>>(
				Expression.Not(
					RewriteLambdaBody(expression, param1, param2)),
				param1, param2);
		}

		public static implicit operator Expression<Func<TEntity, TContext, bool>>(QueryExpression<TEntity, TContext> spec)
		{
			return spec.expression;
		}

		public static implicit operator QueryExpression<TEntity, TContext>(Expression<Func<TEntity, TContext, bool>> query)
		{
			return new QueryExpression<TEntity, TContext>(query);
		}

		static Expression<Func<TEntity, TContext, bool>> Combine(Expression<Func<TEntity, TContext, bool>> self, Expression<Func<TEntity, TContext, bool>> expression, Func<Expression, Expression, Expression> selector)
		{
			if (self == null)
				throw new ArgumentNullException("self");
			if (expression == null)
				throw new ArgumentNullException("expression");

			var param1 = CreateParameterFrom(self, 0);
			var param2 = CreateParameterFrom(self, 1);

			return Expression.Lambda<Func<TEntity, TContext, bool>>(
				selector(
					RewriteLambdaBody(self, param1, param2),
					RewriteLambdaBody(expression, param1, param2)),
				param1, param2);
		}

		static Expression RewriteLambdaBody(LambdaExpression expression, ParameterExpression param1, ParameterExpression param2)
		{
			var bodyExpression = new ParameterRewriter(expression.Parameters[0], param1).Visit(expression.Body);
			return new ParameterRewriter(expression.Parameters[1], param2).Visit(bodyExpression);
		}

		class ParameterRewriter : ExpressionVisitor
		{
			readonly ParameterExpression candidate;
			readonly ParameterExpression replacement;

			public ParameterRewriter(ParameterExpression candidate, ParameterExpression replacement)
			{
				this.candidate = candidate;
				this.replacement = replacement;
			}

			protected override Expression VisitParameter(ParameterExpression expression)
			{
				return ReferenceEquals(expression, candidate) ? replacement : expression;
			}
		}

		static ParameterExpression CreateParameterFrom(Expression<Func<TEntity, TContext, bool>> left, int parameterPosition)
		{
			var template = left.Parameters[parameterPosition];
			return Expression.Parameter(template.Type, template.Name);
		}
	}
}
